from django.utils import simplejson
from django.core.cache import cache
import md5

TYPE_TEXT = 'text'
TYPE_NUMERIC = 'numeric'
TYPE_DATE = 'date'
TYPE_BOOLEAN = 'boolean'
TYPE_LOOKUP = 'lookup'
TYPE_AGGREGATE = 'aggregate'

class SearchCriteria (object):
	field = None
	operator = None
	values = None
	
	def __init__( self, field, operator, values ):
		self.field = field
		self.operator = operator
		self.values = values
	
	def __repr__( self ):
		return '%s %s %s' % (self.field, self.operator, self.values)

class Search (object):
	criteria = None
	display_fields = None
	sort_field = None
	
	def __init__( self, data=None ):
		if data is not None:
			self.parse( data )
		else:
			self.criteria = []
			self.display_fields = []
			self.sort_field = None
	
	def __repr__( self ):
		return '\n'.join( [repr(c) for c in self.criteria] )
	
	def cache_key( self, context=None ):
		""" Returns a unique ID for this search criteria, excluding display and sort fields.
			The optional context parameter can be used to distinguish between two searches with the
			same criteria. See SearchPlugin.get_search_context(). """
		key = repr( self )
		if context is not None:
			key = key + '\n' + str(context)
		return md5.new(key).hexdigest()
	
	def is_sort_formatted( self ):
		""" Returns True if the specified sort field is displayed as a formatted value. This
			allows for sorting result sets based on formatted and unformatted values. """
		from seeker.utils import parse_sort_field, parse_display_field
		if (self.sort_field is None) or (self.display_fields is None):
			return False
		(sort_field, descending) = parse_sort_field( self.sort_field )
		for disp in self.display_fields:
			(display_field, formatted) = parse_display_field( disp )
			if display_field == sort_field:
				return formatted
		return False
	
	def parse( self, data ):
		""" Parses a JSON string, building the SearchCriteria objects. """
		params = simplejson.loads( data )
		self.criteria = []
		for crit in params.get('criteria',[]):
			self.criteria.append( SearchCriteria(crit['field'],crit['operator'],crit['values']) )
		self.display_fields = params.get( 'display_fields', [] )
		self.sort_field = params.get( 'sort_field', None )
	
	def flatten( self ):
		""" Returns a JSON string representation of this search. """
		crit = [ {'field':c.field, 'operator':c.operator, 'values':c.values} for c in self.criteria ]
		return simplejson.dumps( {
			'criteria': crit,
			'display_fields': self.display_fields,
			'sort_field': self.sort_field
		} )

class SearchResult (object):
	""" An interface defining a result set. Note that when the plugin has caching enabled,
		this class must be safe to pickle. """
	
	def count( self ):
		""" Returns the number of rows in this result set. """
		raise NotImplementedError()
	
	def sort( self, field, formatted ):
		""" Sorts the result set by the specified field. If 'formatted' is True, the sort field is
			being displayed as a formatted field and should be sorted by the formatted value.
			Checking should be performed to ensure the results are not re-sorted if it is not
			necessary. """
		raise NotImplementedError()
	
	def get_value( self, rownum, field, formatted ):
		""" Returns a string representation of the field for the given row number. If 'formatted' is
			True, a formatted value should be returned. """
		raise NotImplementedError()
	
	def get_html( self, rownum, field, formatted ):
		""" Returns an HTML string representing the value of the field for the given row number.
			By default, this calls through to get_value(). """
		return self.get_value( rownum, field, formatted )

class SearchPlugin (object):
	
	cache_results = True
	cache_time = 300
	
	def get_search_fields( self, request ):
		""" Returns a list of tuples of the form (group_name,[field_list]) representing fields that are
			available to search on. Groupings will be represented by <optgroup> elements. """
		raise NotImplementedError()
	
	def get_display_fields( self, request ):
		""" Returns a list of tuples of the form (group_name,[field_list]) representing fields that are
			available to be displayed. Groupings will be represented by <optgroup> elements. """
		raise NotImplementedError()
	
	def get_initial_display_fields( self, request ):
		""" Returns a list of fields to be displayed initially. """
		return self.get_display_fields( request )
	
	def get_field_name( self, request, field ):
		""" Returns the human-readable name for the given field. """
		raise NotImplementedError()
	
	def get_field_type( self, request, field ):
		""" Returns the type of the given field. Should be one of the TYPE_* constants. """
		raise NotImplementedError()
	
	def is_field_formatted( self, request, field ):
		""" Returns True if the given field should be initially displayed as a formatted value.
			The search screen will still allow lookup fields to be displayed unformatted. Defaults
			to True for TYPE_LOOKUP fields, False otherwise. """
		return self.get_field_type(request,field) == TYPE_LOOKUP
	
	def is_field_sortable( self, request, field ):
		""" Returns True if results can be sorted by the given field. Defaults to True. """
		return True
	
	def get_default_sort_field( self, request ):
		""" Returns a field to sort by default. Defaults to None. """
		return None
	
	def get_field_operators( self, request, field ):
		""" Returns a list of tuples of the form (oper_code,oper_name). The oper_code will be
			passed back in the SearchCriteria objects. """
		raise NotImplementedError()
	
	def get_field_values( self, request, field ):
		""" Returns a list of tuples of the form (value,label). Should return None if the field is not a lookup.
			The value will be passed back in the SearchCriteria objects. """
		return None
	
	def is_field_importable( self, request, field ):
		""" Returns True if the search interface should show an import widget for this field, False otherwise. """
		return False
	
	def do_import( self, request, field, data ):
		""" Returns a string representation of the parsed file the user imported. """
		try:
			values = data.split( ',' )
			return '; '.join( [ s.strip() for s in values ] )
		except Exception, ex:
			return ''
	
	def get_search_context( self, request, search ):
		""" Returns extra context for caching searches. This could be used, for example, when two users perform
			the same search but get different results, to avoid sharing a single cache entry. """
		return None
	
	def get_results( self, request, search ):
		""" Returns a SearchResult instance, pulling it from the cache if possible (and caching is on). Subclasses
			do not need to implement this method if the default	caching behavior is acceptable. """
		if self.cache_results:
			ctx = self.get_search_context( request, search )
			key = search.cache_key( ctx )
			result = cache.get( key )
			if result is None:
				result = self.do_search( request, search )
				cache.set( key, result, self.cache_time )
			return result
		return do_search( request, search )
	
	def do_search( self, request, search ):
		""" Returns a SearchResult instance representing the results of the specified search. """
		raise NotImplementedError()
	
	def do_count( self, request, search ):
		""" Returns the number of records the specified search would return. """
		raise NotImplementedError()
	
	def save_search( self, request, base_url, search, name ):
		""" Saves a search, and returns a saved search object. """
		raise NotImplementedError()
	
	def load_search( self, request, base_url, search_id ):
		""" Returns a saved search. A saved search must have `id` and `name` attributes. """
		raise NotImplementedError()
	
	def get_searches( self, request, base_url ):
		""" Returns a list of saved searches. The only requirements for a saved search is that it have
			two attributes: `id` and `name` """
		raise NotImplementedError()
